fix: cap unbounded collections and convert panicking index access to … - #268
Merged
Cybermaxi7 merged 1 commit intoAug 20, 2026
Merged
Conversation
…typed errors Closes MarketXpress#260 Two related classes of unvalidated caller input, hardened across the public API: Uncapped collections now rejected with ContractError::TooManyItems: - create_bulk_escrows (new MAX_BULK_ESCROWS_PER_CALL) - create_milestone_escrow (new MAX_MILESTONES_PER_ESCROW) - create_group_buy_escrow (new MAX_GROUP_BUY_BUYERS) - get_escrows now clamps `limit` to a new MAX_PAGE_SIZE instead of scanning as many entries as the caller asks for MAX_PAGE_SIZE and MAX_BULK_ESCROWS_PER_CALL are sized to actually fit within Soroban's per-transaction resource budget (~100 total footprint entries, ~50 write entries) rather than just mirroring the existing MAX_ESCROWS_PER_BATCH=50 constant, which a test caught: a full-size get_escrows(100) or create_bulk_escrows(50) call blows the ledger footprint/write budget and would fail on a real network even after being "capped". Panicking index access converted to typed errors: - release_item: item_index -> ItemNotFound (was a separate bounds check + .unwrap()) - complete_milestone: milestone_index -> MilestoneNotFound (same) - fund_group_buy / withdraw_group_buy_contribution: internal buyer index -> Unauthorized (defensive; index is always in bounds since it's derived from enumerate() over the same vec, but this removes the panic path entirely) Remaining unwrap()/expect() audited per the issue's checklist: - process_seller_transfer's and execute_mediation_settlement's fee collector lookups now return ContractError::InvalidFeeConfig instead of panicking (process_seller_transfer's signature changed to Result<i128, ContractError>; all 5 call sites updated) - check_metadata_access's admin lookup no longer panics if the contract was never initialized - accept_admin's admin lookup returns NotAdmin instead of panicking - create_milestone_escrow / create_group_buy_escrow's post-creation escrow re-fetch returns EscrowNotFound instead of panicking (defensive; the record was just written by create_escrow_internal) - add_i128's overflow expect() is the one remaining unwrap/expect, with a comment explaining why it's unreachable in practice (global i128 counters, bounded by real token supply) Adds 5 new tests: complete_milestone with an out-of-range index, and at-the-limit/over-the-limit coverage for each of the four new caps (milestone count, bulk escrow count, group-buy buyer count, and get_escrows page size). 128 unit tests + 2 integration tests pass; cargo fmt, clippy -D warnings, and the optimized wasm build are clean.
Closed
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TooManyItemserror, following the existingMAX_ITEMS_PER_ESCROWprecedent:get_escrows(limit)— newMAX_PAGE_SIZE(50); over-limit values are silently clamped rather than erroring, since pagination is a read-only view.batch_collect_fees(escrow_ids)— already capped byMAX_ESCROWS_PER_BATCHfrom [high] batch_collect_fees reports collected fees but transfers no funds #259; verified still enforced.create_bulk_escrows(requests)— newMAX_BULK_ESCROWS_PER_CALL(20).create_milestone_escrow(milestones)— newMAX_MILESTONES_PER_ESCROW(50).create_group_buy_escrow(buyers)— newMAX_GROUP_BUY_BUYERS(50).MAX_PAGE_SIZEandMAX_BULK_ESCROWS_PER_CALLaren't arbitrary — they're sized to actually fit inside Soroban's per-transaction resource budget (~100 total footprint entries, ~50 write entries). A first pass at 100/50 looked "capped" but still blew the real ledger footprint/write budget in testing (a full-size call would fail on a real network even after the cap), so both were brought down with headroom..get(i).unwrap()) into a typed error instead:release_item→ItemNotFound(collapsed a separate bounds check +unwrap()into one.get(...).ok_or(...)?)complete_milestone→MilestoneNotFound(same pattern)fund_group_buy/withdraw_group_buy_contribution→Unauthorizedon their internal buyer-contribution lookup (defensive — the index is derived fromenumerate()over the same vec so it's always in bounds, but this removes the panic path entirely)unwrap()/expect()inlib.rsper the issue's checklist:process_seller_transferandexecute_mediation_settlement's fee-collector lookups now returnContractError::InvalidFeeConfiginstead of panicking if the contract was never initialized (process_seller_transfer's signature changed toResult<i128, ContractError>; all 5 call sites updated to propagate with?)check_metadata_access's admin lookup no longer panics on an uninitialized contractaccept_admin's admin lookup returnsNotAdmininstead of panickingcreate_milestone_escrow/create_group_buy_escrow's post-creation escrow re-fetch returnsEscrowNotFoundinstead of panicking (defensive — the record was just written bycreate_escrow_internal)expect()(add_i128's counter-overflow guard) is left as-is with a comment explaining why it's unreachable in practice: it's a globali128analytics counter incremented by amounts bounded by real token supply, many orders of magnitude belowi128::MAX.complete_milestoneindex, plus at-the-limit / over-the-limit coverage for each of the four new caps (milestones, bulk escrows, group-buy buyers,get_escrowspage size).Scope note
The issue named
batch_collect_feesandget_escrowsexplicitly. While implementing the fix I found the same unbounded-collection bug in three more entrypoints (create_bulk_escrows,create_milestone_escrow,create_group_buy_escrow) and fixed those too, since they're the identical bug class the issue is about ("harden all caller-supplied input"). Flagging this in case reviewers want it split out.Linked Issue
Closes #260
CI Checklist
cargo fmt --all -- --checkcargo clippy --all-targets -- -D warningscargo test— 128 unit tests + 2 integration tests pass (was 123 unit tests before this PR)./scripts/build_wasm.shNotes for Reviewers
process_seller_transferchanging from-> i128to-> Result<i128, ContractError>is the widest-blast-radius change here — it's called fromrelease_escrow,release_item,claim_disputed_funds,complete_milestone, andtrigger_time_lock_release. All five call sites just gained a?; no behavior changes on the success path.fund_group_buy/withdraw_group_buy_contributionindex conversions are defensive rather than fixing a reachable bug — there's no publicindexparameter on those functions, the index is found internally viaenumerate()over the buyer list. Included since the issue's technical context explicitly calls out those two line locations.InvalidFeeConfig,TooManyItems,EscrowNotFound,Unauthorized,NotAdmin) at existing variants, no new variants added.